package com.bumptech.glide.load.resource.bitmap;

import com.bumptech.glide.load.*;
import com.bumptech.glide.load.ImageHeaderParser.ImageType;
import com.bumptech.glide.load.engine.Resource;
import com.bumptech.glide.load.engine.bitmap_recycle.ArrayPool;
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy.SampleSizeRounding;
import com.bumptech.glide.request.RequestOptions;
import com.bumptech.glide.request.target.Target;
import com.bumptech.glide.util.LogTime;
import com.bumptech.glide.util.LogUtil;
import com.bumptech.glide.util.Preconditions;
import com.bumptech.glide.util.Util;
import ohos.agp.window.service.DisplayAttributes;
import ohos.media.image.ImageSource;
import ohos.media.image.PixelMap;
import ohos.media.image.common.ColorSpace;
import ohos.media.image.common.PixelFormat;
import ohos.media.image.common.Rect;
import ohos.media.image.common.Size;
import org.jetbrains.annotations.Nullable;

import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.util.*;

/**
 * Downsamples, decodes, and rotates images according to their exif orientation using BitmapFactory.
 */
public final class Downsampler {
  static final String TAG = "Downsampler";

  /**
   * Indicates the {@link com.bumptech.glide.load.DecodeFormat} that will be used in conjunction
   * with the image format to determine the Bitmap.Config to provide to
   * BitmapFactory.Options#inPreferredConfig when decoding the image.
   */
  public static final Option<DecodeFormat> DECODE_FORMAT =
      Option.memory(
          "com.bumptech.glide.load.resource.bitmap.Downsampler.DecodeFormat", DecodeFormat.DEFAULT);

  /**
   * Sets the {@link PreferredColorSpace} that will be used along with the version of openharmony and
   * color space of the requested image to determine the final color space used to decode the image.
   *
   * <p>Refer to {@link PreferredColorSpace} for details on how this option works and its various
   * limitations.
   */
  public static final Option<PreferredColorSpace> PREFERRED_COLOR_SPACE =
      Option.memory(
          "com.bumptech.glide.load.resource.bitmap.Downsampler.PreferredColorSpace",
          PreferredColorSpace.SRGB);
  /**
   * Indicates the {@link com.bumptech.glide.load.resource.bitmap.DownsampleStrategy} option that
   * will be used to calculate the sample size to use to downsample an image given the original and
   * target dimensions of the image.
   *
   * @deprecated Use {@link DownsampleStrategy#OPTION} directly instead.
   */
  @Deprecated
  public static final Option<DownsampleStrategy> DOWNSAMPLE_STRATEGY = DownsampleStrategy.OPTION;
  /**
   * Ensure that the size of the bitmap is fixed to the requested width and height of the resource
   * from the caller. The final resource dimensions may differ from the requested width and height,
   * and thus setting this to true may result in the bitmap size differing from the resource
   * dimensions.
   *
   * <p>This can be used as a performance optimization for KitKat and above by fixing the size of
   * the bitmap for a collection of requested resources so that the bitmap pool will not need to
   * allocate new bitmaps for images of different sizes.
   */
  // Public API
  @SuppressWarnings("WeakerAccess")
  public static final Option<Boolean> FIX_BITMAP_SIZE_TO_REQUESTED_DIMENSIONS =
      Option.memory("com.bumptech.glide.load.resource.bitmap.Downsampler.FixBitmapSize", false);

  /**
   * Indicates that it's safe or unsafe to decode Bitmaps with Bitmap.Config#HARDWARE.
   *
   * <p>Callers should almost never set this value to {@code true} manually. Glide will already do
   * so when Glide believes it's safe to do (when no transformations are applied). Instead, callers
   * can set this value to {@code false} to prevent Glide from decoding hardware bitmaps if Glide is
   * unable to detect that hardware bitmaps are unsafe. For example, you should set this to {@code
   * false} if you plan to draw it to a software {@link ohos.agp.render.Canvas} or if you plan to
   * inspect the Bitmaps pixels with Bitmap#getPixel(int, int) or Bitmap#getPixels(int[], int, int, int, int, int, int).
   *
   * <p>Callers can disable hardware Bitmaps for all loads using {@link
   * com.bumptech.glide.GlideBuilder#setDefaultRequestOptions(RequestOptions)}.
   *
   */
  public static final Option<Boolean> ALLOW_HARDWARE_CONFIG =
      Option.memory(
          "com.bumptech.glide.load.resource.bitmap.Downsampler.AllowHardwareDecode", false);

  private static final String WBMP_MIME_TYPE = "image/vnd.wap.wbmp";
  private static final String ICO_MIME_TYPE = "image/x-ico";
  private static final Set<String> NO_DOWNSAMPLE_PRE_N_MIME_TYPES =
      Collections.unmodifiableSet(new HashSet<>(Arrays.asList(WBMP_MIME_TYPE, ICO_MIME_TYPE)));
  private static final DecodeCallbacks EMPTY_CALLBACKS =
      new DecodeCallbacks() {
        @Override
        public void onObtainBounds() {
          // Do nothing.
        }

        @Override
        public void onDecodeComplete(BitmapPool pixelmapPool, PixelMap downsampled) {
          // Do nothing.
        }
      };
  private static final Set<ImageHeaderParser.ImageType> TYPES_THAT_USE_POOL_PRE_KITKAT =
      Collections.unmodifiableSet(
          EnumSet.of(
              ImageHeaderParser.ImageType.JPEG,
              ImageHeaderParser.ImageType.PNG_A,
              ImageHeaderParser.ImageType.PNG));
  private static final Queue<ImageSource.DecodingOptions> OPTIONS_QUEUE = Util.createQueue(0);

  private final BitmapPool bitmapPool;
  private final DisplayAttributes displayMetrics;
  private final ArrayPool byteArrayPool;
  private final List<ImageHeaderParser> parsers;
  private final HardwareConfigState hardwareConfigState = HardwareConfigState.getInstance();

  public Downsampler(
      List<ImageHeaderParser> parsers,
      DisplayAttributes displayMetrics,
      BitmapPool bitmapPool,
      ArrayPool byteArrayPool) {
    this.parsers = parsers;
    this.displayMetrics = Preconditions.checkNotNull(displayMetrics);
    this.bitmapPool = Preconditions.checkNotNull(bitmapPool);
    this.byteArrayPool = Preconditions.checkNotNull(byteArrayPool);
  }

  public boolean handles(@SuppressWarnings("unused") InputStream is) {
    // We expect Downsampler to handle any available type openharmony supports.
    return true;
  }

  public boolean handles(@SuppressWarnings("unused") ByteBuffer byteBuffer) {
    // We expect downsampler to handle any available type openharmony supports.
    return true;
  }


  /**
   * Returns a Bitmap decoded from the given {@link InputStream} that is rotated to match any EXIF
   * data present in the stream and that is downsampled according to the given dimensions and any
   * provided {@link com.bumptech.glide.load.resource.bitmap.DownsampleStrategy} option.
   *
   * @see #decode(InputStream, int, int, Options, DecodeCallbacks)
   * @param options
   * @param is
   * @param outWidth
   * @param outHeight
   * @return resource
   * @throws IOException
   */
  public Resource<PixelMap> decode(InputStream is, int outWidth, int outHeight, Options options)
      throws IOException {
    return decode(is, outWidth, outHeight, options, EMPTY_CALLBACKS);
  }

  /**
   * Returns a Bitmap decoded from the given {@link InputStream} that is rotated to match any EXIF
   * data present in the stream and that is downsampled according to the given dimensions and any
   * provided {@link com.bumptech.glide.load.resource.bitmap.DownsampleStrategy} option.
   *
   * <p>If a Bitmap is present in the {@link
   * com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool} whose dimensions exactly match those
   * of the image for the given InputStream is available, the operation is much less expensive in
   * terms of memory.
   *
   * @param is An {@link InputStream} to the data for the image.
   * @param requestedWidth The width the final image should be close to.
   * @param requestedHeight The height the final image should be close to.
   * @param options A set of options that may contain one or more supported options that influence
   *     how a Bitmap will be decoded from the given stream.
   * @param callbacks A set of callbacks allowing callers to optionally respond to various
   *     significant events during the decode process.
   * @return A new bitmap containing the image from the given InputStream, or recycle if recycle is
   *     not null.
   * @throws IOException
   */
  public Resource<PixelMap> decode(
      InputStream is,
      int requestedWidth,
      int requestedHeight,
      Options options,
      DecodeCallbacks callbacks)
      throws IOException {
    return decode(
        new ImageReader.InputStreamImageReader(is, parsers, byteArrayPool),
        requestedWidth,
        requestedHeight,
        options,
        callbacks);
  }


  private Resource<PixelMap> decode(
      ImageReader imageReader,
      int requestedWidth,
      int requestedHeight,
      Options options,
      DecodeCallbacks callbacks)
      throws IOException {
    //Note: openharmony doesnot support inTempStorage
    ImageSource.DecodingOptions decodingOpts = getDefaultOptions();

    DecodeFormat decodeFormat = options.get(DECODE_FORMAT);
    PreferredColorSpace preferredColorSpace = options.get(PREFERRED_COLOR_SPACE);
    DownsampleStrategy downsampleStrategy = options.get(DownsampleStrategy.OPTION);
    boolean fixBitmapToRequestedDimensions = options.get(FIX_BITMAP_SIZE_TO_REQUESTED_DIMENSIONS);
    boolean isHardwareConfigAllowed =
        options.get(ALLOW_HARDWARE_CONFIG) != null && options.get(ALLOW_HARDWARE_CONFIG);

    try {
      PixelMap result =
          decodeFromWrappedStreams(
              imageReader,
              decodingOpts,
              downsampleStrategy,
              decodeFormat,
              preferredColorSpace,
              isHardwareConfigAllowed,
              requestedWidth,
              requestedHeight,
              fixBitmapToRequestedDimensions,
              callbacks);
      return BitmapResource.obtain(result, bitmapPool);
    } finally {
      releaseOptions(decodingOpts);
    }
  }

  private PixelMap decodeFromWrappedStreams(
      ImageReader imageReader,
      ImageSource.DecodingOptions decodingOpts,
      DownsampleStrategy downsampleStrategy,
      DecodeFormat decodeFormat,
      PreferredColorSpace preferredColorSpace,
      boolean isHardwareConfigAllowed,
      int requestedWidth,
      int requestedHeight,
      boolean fixBitmapToRequestedDimensions,
      DecodeCallbacks callbacks)
      throws IOException {
    long startTime = LogTime.getLogTime();

    int[] sourceDimensions = getDimensions(imageReader, decodingOpts, callbacks, bitmapPool);
    int sourceWidth = sourceDimensions[0];
    int sourceHeight = sourceDimensions[1];
    //String sourceMimeType = options.outMimeType;  //TODO:
    // If we failed to obtain the image dimensions, we may end up with an incorrectly sized Bitmap,
    // so we want to use a mutable Bitmap type. One way this can happen is if the image header is so
    // large (10mb+) that our attempt to use inJustDecodeBounds fails and we're forced to decode the
    // full size image.
    if (sourceWidth == -1 || sourceHeight == -1) {
      isHardwareConfigAllowed = false;
    }

    int orientation = imageReader.getImageOrientation();
    int degreesToRotate = TransformationUtils.getExifOrientationDegrees(orientation);
    boolean isExifOrientationRequired = TransformationUtils.isExifOrientationRequired(orientation);

    int targetWidth =
        requestedWidth == Target.SIZE_ORIGINAL
            ? (isRotationRequired(degreesToRotate) ? sourceHeight : sourceWidth)
            : requestedWidth;
    int targetHeight =
        requestedHeight == Target.SIZE_ORIGINAL
            ? (isRotationRequired(degreesToRotate) ? sourceWidth : sourceHeight)
            : requestedHeight;

    ImageType imageType = imageReader.getImageType();

    calculateScaling(
        imageType,
        imageReader,
        callbacks,
        bitmapPool,
        downsampleStrategy,
        degreesToRotate,
        sourceWidth,
        sourceHeight,
        targetWidth,
        targetHeight,
        decodingOpts);
    calculateConfig(
        imageReader,
        decodeFormat,
        isHardwareConfigAllowed,
        isExifOrientationRequired,
        decodingOpts,
        targetWidth,
        targetHeight);
    PixelMap downsampled = decodeStream(imageReader, decodingOpts, false, callbacks, bitmapPool);
    callbacks.onDecodeComplete(bitmapPool, downsampled);

    PixelMap rotated = null;
    if (downsampled != null) {
      // If we scaled, the Bitmap density will be our inTargetDensity. Here we correct it back to
      // the expected density dpi.
      downsampled.setBaseDensity(displayMetrics.densityDpi);

      rotated = TransformationUtils.rotateImageExif(bitmapPool, downsampled, orientation);
      if (!downsampled.equals(rotated)) {
        bitmapPool.put(downsampled);
      }
    }

    return rotated;
  }

  private static void calculateScaling(
      ImageType imageType,
      ImageReader imageReader,
      DecodeCallbacks decodeCallbacks,
      BitmapPool bitmapPool,
      DownsampleStrategy downsampleStrategy,
      int degreesToRotate,
      int sourceWidth,
      int sourceHeight,
      int targetWidth,
      int targetHeight,
      ImageSource.DecodingOptions options)
      throws IOException {
    // We can't downsample source content if we can't determine its dimensions.
    if (sourceWidth <= 0 || sourceHeight <= 0) {
      if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
        LogUtil.info(
            TAG,
            "Unable to determine dimensions for: "
                + imageType
                + " with target ["
                + targetWidth
                + "x"
                + targetHeight
                + "]");
      }
      return;
    }

    int orientedSourceWidth = sourceWidth;
    int orientedSourceHeight = sourceHeight;


    final float exactScaleFactor =
        downsampleStrategy.getScaleFactor(
            orientedSourceWidth, orientedSourceHeight, targetWidth, targetHeight);

    if (exactScaleFactor <= 0f) {
      throw new IllegalArgumentException(
          "Cannot scale with factor: "
              + exactScaleFactor
              + " from: "
              + downsampleStrategy
              + ", source: ["
              + sourceWidth
              + "x"
              + sourceHeight
              + "]"
              + ", target: ["
              + targetWidth
              + "x"
              + targetHeight
              + "]");
    }

    SampleSizeRounding rounding =
        downsampleStrategy.getSampleSizeRounding(
            orientedSourceWidth, orientedSourceHeight, targetWidth, targetHeight);
    if (rounding == null) {
      throw new IllegalArgumentException("Cannot round with null rounding");
    }

    int outWidth = round(exactScaleFactor * orientedSourceWidth);
    int outHeight = round(exactScaleFactor * orientedSourceHeight);

    int widthScaleFactor = orientedSourceWidth / outWidth;
    int heightScaleFactor = orientedSourceHeight / outHeight;

    // TODO: This isn't really right for both CenterOutside and CenterInside. Consider allowing
    // DownsampleStrategy to pick, or trying to do something more sophisticated like picking the
    // scale factor that leads to an exact match.
    int scaleFactor =
        rounding == SampleSizeRounding.MEMORY
            ? Math.max(widthScaleFactor, heightScaleFactor)
            : Math.min(widthScaleFactor, heightScaleFactor);


    if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
      LogUtil.info(
          TAG,
          "Calculate scaling"
              + ", source: ["
              + sourceWidth
              + "x"
              + sourceHeight
              + "]"
              + ", degreesToRotate: "
              + degreesToRotate
              + ", target: ["
              + targetWidth
              + "x"
              + targetHeight
              + "]"
              + ", power of two scaled: ["
              + ", exact scale factor: "
              + exactScaleFactor
              + ", target density: ");
    }
  }

  /**
   * BitmapFactory calculates the density scale factor as a float. This introduces some non-trivial
   * error. This method attempts to account for that error by adjusting the inTargetDensity so that
   * the final scale factor is as close to our target as possible.
   * @param adjustedScaleFactor
   * @return int
   */
  private static int adjustTargetDensityForError(double adjustedScaleFactor) {
    int densityMultiplier = getDensityMultiplier(adjustedScaleFactor);
    int targetDensity = round(densityMultiplier * adjustedScaleFactor);
    float scaleFactorWithError = targetDensity / (float) densityMultiplier;
    double difference = adjustedScaleFactor / scaleFactorWithError;
    return round(difference * targetDensity);
  }

  private static int getDensityMultiplier(double adjustedScaleFactor) {
    return (int)
        Math.round(
            Integer.MAX_VALUE
                * (adjustedScaleFactor <= 1D ? adjustedScaleFactor : 1 / adjustedScaleFactor));
  }

  // This is weird, but it matches the logic in a bunch of openharmony views/framework classes for
  // rounding.
  private static int round(double value) {
    return (int) (value + 0.5d);
  }

  private boolean shouldUsePool(ImageType imageType) {

      return true;
  }

  @SuppressWarnings("deprecation")
  private void calculateConfig(
      ImageReader imageReader,
      DecodeFormat format,
      boolean isHardwareConfigAllowed,
      boolean isExifOrientationRequired,
      ImageSource.DecodingOptions optionsWithScaling,
      int targetWidth,
      int targetHeight) {

    boolean hasAlpha = false;
    try {
      hasAlpha = imageReader.getImageType().hasAlpha();
    } catch (IOException e) {
      if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
        LogUtil.info(
            TAG,
            "Cannot determine whether the image has alpha or not from header"
                + ", format "
                + format);
      }
    }
  }

  private static int[] getDimensions(
      ImageReader imageReader,
      ImageSource.DecodingOptions decodingOpts,
      DecodeCallbacks decodeCallbacks,
      BitmapPool bitmapPool)
      throws IOException {

    decodeStream(imageReader, decodingOpts, true, decodeCallbacks, bitmapPool);
    return new int[] {decodingOpts.desiredSize.width, decodingOpts.desiredSize.height};
  }

  private static PixelMap decodeStream(
      ImageReader imageReader,
      ImageSource.DecodingOptions decodingOpts,
      boolean getimginfo,
      DecodeCallbacks callbacks,
      BitmapPool bitmapPool)
      throws IOException {
    if (!getimginfo) {
      // Once we've read the image header, we no longer need to allow the buffer to expand in
      // size. To avoid unnecessary allocations reading image data, we fix the mark limit so that it
      // is no larger than our current buffer size here. We need to do so immediately before
      // decoding the full image to avoid having our mark limit overridden by other calls to
      // mark and reset. See issue #225.
      callbacks.onObtainBounds();
      imageReader.stopGrowingBuffers();
    }

    // BitmapFactory.Options out* variables are reset by most calls to decodeStream, successful or
    // otherwise, so capture here in case we log below.
    int sourceWidth = decodingOpts.desiredSize.width;
    int sourceHeight = decodingOpts.desiredSize.height;
    final PixelMap result;
    TransformationUtils.getBitmapDrawableLock().lock();
    try {
      result = imageReader.decodeBitmap(decodingOpts, null, getimginfo);  //TODO: , need to set formatHint
    } catch (IllegalArgumentException e) {
      IOException bitmapAssertionException =
          newIoExceptionForInBitmapAssertion(e, sourceWidth, sourceHeight, decodingOpts);
      if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
        LogUtil.info(
            TAG,
            "Failed to decode with inBitmap, trying again without Bitmap re-use");
      }

      throw bitmapAssertionException;
    } finally {
      TransformationUtils.getBitmapDrawableLock().unlock();
    }

    return result;
  }


  private static String getInBitmapString(ImageSource.DecodingOptions options) {
    return getBitmapString();
  }

  
  @Nullable
  private static String getBitmapString() {
  
    return "[There is no inbitmap in HMOS"  + "] ";
  }

  // BitmapFactory throws an IllegalArgumentException if any error occurs attempting to decode a
  // file when inBitmap is non-null, including those caused by partial or corrupt data. We still log
  // the error because the IllegalArgumentException is supposed to catch errors reusing Bitmaps, so
  // want some useful log output. In most cases this can be safely treated as a normal IOException.
  private static IOException newIoExceptionForInBitmapAssertion(
      IllegalArgumentException e,
      int outWidth,
      int outHeight,
      ImageSource.DecodingOptions options) {
    return new IOException(
        "Exception decoding bitmap"
            + ", outWidth: "
            + outWidth
            + ", outHeight: "
            + outHeight
            + ", inBitmap: "
            + getInBitmapString(options),
        e);
  }

  private static void setInBitmap(
      ImageSource.DecodingOptions decodingOpts, BitmapPool pixelmapPool, int width, int height) {

    // There is no HARDWARE CONFIG in openharmony
    //Note:openharmony imageSource.createPixelmap() does not take any caching bitmap, so not set any inBitmap
    //TODO: IMP check need to set width and height
  }

  private static synchronized ImageSource.DecodingOptions getDefaultOptions() {
    ImageSource.DecodingOptions decodingOpts;
    synchronized (OPTIONS_QUEUE) {
      decodingOpts = OPTIONS_QUEUE.poll();
    }
    if (decodingOpts == null) {
      decodingOpts = new ImageSource.DecodingOptions();
      resetOptions(decodingOpts);
    }

    return decodingOpts;
  }

  private static void releaseOptions(ImageSource.DecodingOptions decodingOpts) {  
    resetOptions(decodingOpts);
    synchronized (OPTIONS_QUEUE) {
      OPTIONS_QUEUE.offer(decodingOpts);
    }
  }

  private static void resetOptions(ImageSource.DecodingOptions decodingOpts) {
    decodingOpts.desiredSize = new Size(0, 0);
    decodingOpts.desiredRegion = new Rect(0, 0, 0, 0);
    decodingOpts.desiredPixelFormat = PixelFormat.ARGB_8888;
    decodingOpts.allowPartialImage = false;
    decodingOpts.desiredColorSpace = ColorSpace.UNKNOWN;
    decodingOpts.rotateDegrees = 0.0F;
    decodingOpts.sampleSize = 1;
   
  }

  /** Callbacks for key points during decodes. */
  public interface DecodeCallbacks {
    void onObtainBounds();

    void onDecodeComplete(BitmapPool pixelmapPool, PixelMap downsampled) throws IOException;
  }

  private static boolean isRotationRequired(int degreesToRotate) {
    return degreesToRotate == 90 || degreesToRotate == 270;
  }
}
